import { $render, Logger } from '@/utils/logger';
import { deserializePos } from '../pos';
import crossRules from './crossRules';
import { MoveOpt } from './types';
export { setCreepStand } from './crossRules';

/**
 * 房间移动成本缓存
 *
 * 会缓存房间内的静态地形、道路、建筑等短时间内不会移动的对象
 * 如果出现了撞墙等情况，说明缓存过期，会在撞墙时移除缓存以便下次重新搜索
 */
export const costCache: { [roomName: string]: CostMatrix } = {};

/**
 * 路径缓存
 *
 * Creep 在执行远程寻路时会优先检查该缓存
 * 键为路径的起点和终点名，例如："12/32/W1N1 23/12/W2N2"，值是使用 serializeFarPath 序列化后的路径
 */
export let routeCache: { [routeKey: string]: string } = {};

/**
 * 路径点缓存
 *
 * Creep 会把自己下一个路径点对应的位置缓存在这里，这样就不用每 tick 都从内存中的路径点字符串重建位置
 * 不过这么做会导致 creep 无法立刻感知到位置的变化
 *
 * 其键为 creep 的名字，值为下一个路径目标
 */
const wayPointCache: { [creepName: string]: RoomPosition } = {};

/**
 * 移动 creep
 * 平均执行消耗 0.220 ~ 0.232（不包含寻路消耗）
 *
 * @param creep 要进行移动的 creep
 * @param targetPos 要移动到的目标位置
 * @param moveOpt 移动参数
 */
export const goTo = function (creep: Creep | PowerCreep, targetPos: RoomPosition | undefined, moveOpt: MoveOpt = {}): ScreepsReturnCode {
    // 如果没有腿子就不移动
    if (creep instanceof Creep && creep.getActiveBodyparts(MOVE) === 0) {
        return ERR_NO_BODYPART;
    }
    // @ts-ignore 如果疲劳不移动
    if (creep.fatigue) {
        return ERR_TIRED;
    }

    // 默认会检查目标变更以及重用路径
    const options: MoveOpt = _.defaults({
        checkTarget: true,
        reusePath: 20
    }, moveOpt);

    if (!creep.memory._go) creep.memory._go = {};
    const moveMemory = creep.memory._go;
    // 如果没有指定目标的话则默认为路径模式
    const target = targetPos || getTarget(creep);
    if (!target) return ERR_INVALID_ARGS;

    const currentPos = `${creep.pos.x}/${creep.pos.y}`;

    // 确认目标有没有变化, 变化了则重新规划路线
    if (options.checkTarget) {
        const targetPosTag = target.serialize();

        if (targetPosTag !== moveMemory.targetPos) {
            moveMemory.targetPos = targetPosTag;
            delete moveMemory.path;
            delete moveMemory.prePos;
        }
    }

    // 有 lastMove 说明已经在移动了，检查上一 tick 移动是否成功
    // （因为上一步的移动结果在这一 tick 开始时才会更新，所以要先检查之前移动是否成功，然后再决定是否要继续移动）
    if (moveMemory.lastMove) {
        // 如果和之前位置重复了就分析撞上了啥
        if (moveMemory.prePos && currentPos === moveMemory.prePos) {
            // 获取前方位置上的 creep（fontCreep）
            const fontPos = directionToPos(creep.pos, moveMemory.lastMove);
            if (!fontPos) {
                delete moveMemory.path;
                delete moveMemory.prePos;
                return ERR_INVALID_TARGET;
            }

            const fontCreep = fontPos.lookFor(LOOK_CREEPS)[0] || fontPos.lookFor(LOOK_POWER_CREEPS)[0];

            // 前方不是 creep 或者不是自己的 creep 或者内存被清空（正在跨越 shard）的话就不会发起对穿
            if (!fontCreep || !fontCreep.my || Object.keys(fontCreep.memory).length <= 0) {
                delete moveMemory.path;
                delete moveMemory.prePos;
                // 撞地形上了说明房间 cost 过期了
                delete costCache[creep.room.name];
                routeCache = {};

                return ERR_INVALID_TARGET;
            }
            // 尝试对穿，如果自己禁用了对穿的话则直接重新寻路
            const crossResult = options.disableCross ? ERR_BUSY : mutualCross(creep, moveMemory.lastMove, fontCreep);

            // 对穿失败说明撞墙上了或者前面的 creep 拒绝对穿
            if (crossResult === ERR_BUSY) {
                /*
                // todo implement this logic
                // feat 拒绝对穿，可以考虑推开前面的爬
                if (requirePush(fontCreep, moveMemory.lastMove, creep) !== OK) {
                    // push 失败，重新寻路
                    moveMemory.path = findPath(creep, targetPos, { ...options, disableRouteCache: true });
                    delete moveMemory.prePos;
                }
                */
                moveMemory.path = findPath(creep, targetPos, { ...options, disableRouteCache: true });
                delete moveMemory.prePos;
            }
            else if (crossResult !== OK) {
                delete moveMemory.path;
                delete moveMemory.prePos;
                // 不知道撞到了啥，反正重新加载房间缓存
                delete costCache[creep.room.name];
                routeCache = {};
            }

            // 对穿失败，需要重新寻路，不需要往下继续执行
            // 对穿成功，相当于重新执行了上一步，也不需要继续往下执行
            return crossResult;
        }

        // 验证通过，没有撞停，继续下一步
        delete moveMemory.lastMove;
    }

    // 如果没有路径的话就重新发起搜索
    if (!moveMemory.path)
        moveMemory.path = findPath(creep, target, options);

    // 还为空的话就是没找到路径或者已经到了
    if (!moveMemory.path) {
        // 到达目的地后如果是路径模式的话就需要更新路径点
        if (!targetPos) updateWayPoint(creep);
        return OK;
    }

    // 使用缓存进行移动
    const direction = <DirectionConstant>Number(moveMemory.path[0]);
    const goResult = creep.move(direction);
    // 移动成功，更新路径
    if (goResult === OK) {
        // 移动到终点了，不需要再检查位置是否重复
        if (moveMemory.path.length === 0) {
            delete moveMemory.lastMove;
        }
        else {
            moveMemory.lastMove = Number(moveMemory.path[0]) as DirectionConstant;
            moveMemory.path = moveMemory.path.substr(1);
        }
    }
    // 其他异常直接报告
    else if (goResult !== ERR_TIRED && goResult !== ERR_BUSY) {
        Logger.warn(`${creep.name} 移动失败，原因：${goResult}`, 'move');
    }

    // 更新最后位置
    moveMemory.prePos = currentPos;

    return goResult;
};

export type Goto = typeof goTo;

/**
 * 路径模式下获取要移动到的目标
 *
 * 会进行缓存
 * 如果内存中没有设置的话则返回 undefined
 */
const getTarget = function (creep: Creep | PowerCreep): RoomPosition {
    // 检查缓存
    let target = wayPointCache[creep.name];
    if (target) return target;

    const memroy = creep.memory._go;
    if (!memroy) return;

    // 优先用路径旗帜
    if (memroy.wayPointFlag) {
        const flag = Game.flags[memroy.wayPointFlag];
        target = flag?.pos;
    }
    // 没有🚩就找找路径数组
    else if (memroy.wayPoints && memroy.wayPoints.length > 0) {
        const [x, y, roomName] = memroy.wayPoints[0].split(' ');
        if (!x || !y || !roomName) {
            Logger.warn(`${creep.mark} 配置了错误的路径点 ${memroy.wayPoints[0]}`, 'move');
        }
        else {
            target = new RoomPosition(~~x, ~~y, roomName);
        }
    }

    wayPointCache[creep.name] = target;

    // 如果还没有找到目标的话说明路径点失效了，移除整个缓存
    if (!target) delete creep.memory._go;

    return target;
};

/**
 * 给 Creep 设置路径点目标
 *
 * target 是一个路径数组或者路径旗帜
 *
 * @param target 路径点目标
 */
export const setWayPoint = function (creep: Creep | PowerCreep, target: string[] | string) {
    if (!creep.memory._go) creep.memory._go = {};
    delete wayPointCache[creep.name];

    // 设置时会移除另一个路径模式的数据，防止这个移动完之后再回头走之前留下的路径点
    if (target instanceof Array) {
        creep.memory._go.wayPoints = target;
        delete creep.memory._go.wayPointFlag;
    }
    else {
        creep.memory._go.wayPointFlag = target + '0';
        delete creep.memory._go.wayPoints;
    }

    return OK;
};

/**
 * 更新路径点
 *
 * 当抵达当前路径点后就需要更新内存数据以移动到下一个路径点
 */
const updateWayPoint = function (creep: Creep | PowerCreep) {
    if (!creep.memory._go) creep.memory._go = {};
    const memory = creep.memory._go;

    if (memory.wayPoints) {
        // 弹出已经抵达的路径点
        if (memory.wayPoints.length > 0) memory.wayPoints.shift();
    }
    else if (memory.wayPointFlag) {
        const preFlag = Game.flags[memory.wayPointFlag];

        // 如果旗帜内存里指定了下一个路径点名称的话就直接使用
        if (preFlag && preFlag.memory && preFlag.memory.next) {
            memory.wayPointFlag = preFlag.memory.next;
        }
        // 否则就默认自增编号
        else {
            // 获取路径旗帜名
            const flagPrefix = memory.wayPointFlag.slice(0, memory.wayPointFlag.length - 1);
            // 把路径旗帜的编号 + 1
            const nextFlagCode = Number(memory.wayPointFlag.substr(-1)) + 1;
            // 把新旗帜更新到内存，这里没有检查旗帜是否存在
            // 原因在于跨 shard 需要在跨越之前将旗帜更新到下一个，但是这时还没有到下个 shard，就获取不到位于下个 shard 的旗帜
            memory.wayPointFlag = flagPrefix + nextFlagCode;
        }
    }

    // 移除缓存以便下次可以重新查找目标
    delete wayPointCache[creep.name];
};

/**
 * 向指定方向发起对穿
 *
 * @param creep 发起对穿的 creep
 * @param direction 要进行对穿的方向
 * @param frontCreep 要被对穿的 creep
 *
 * @returns OK 成功对穿
 * @returns ERR_BUSY 对方拒绝对穿
 * @returns ERR_INVALID_TARGET 前方没有 creep
 */
const mutualCross = function (creep: Creep | PowerCreep, direction: DirectionConstant, frontCreep: Creep | PowerCreep): OK | ERR_BUSY | ERR_INVALID_TARGET {
    creep.say('👉');
    // creep.log('发起对穿！' + fontCreep.pos)

    // 如果前面的 creep 同意对穿了，自己就朝前移动
    const oppositDirection = /* 取相反方向 */((direction + 3) % 8 + 1) as DirectionConstant;
    const frontMoveRet = requireCross(frontCreep, oppositDirection, creep);
    if (frontMoveRet !== OK) return ERR_BUSY;

    const selfMoveResult = creep.move(direction);
    return (selfMoveResult === OK && frontMoveRet === OK) ? OK : ERR_BUSY;
};

/**
 * 请求对穿
 * 自己内存中 stand 为 true 时将拒绝对穿
 *
 * @param creep 被请求对穿的 creep
 * @param direction 请求该 creep 进行对穿
 * @param requireCreep 发起请求的 creep
 */
const requireCross = function (creep: AnyCreep, direction: DirectionConstant, requireCreep: AnyCreep): ScreepsReturnCode {
    // creep 下没有 memory 说明 creep 已经凉了，直接移动即可
    if (!creep.memory) return OK;

    // 动态检查，如果设置了 disableCross 就拒绝
    if (creep.memory.disableCross) {
        creep.say('✋');
        return ERR_BUSY;
    }

    // 如果 requireCreep 有 fatigue，就拒绝
    if ((requireCreep instanceof Creep) && requireCreep.fatigue > 0) {
        creep.say('✋');
        return ERR_BUSY;
    }

    // 如果对穿方向与自己的移动方向相反，也拒绝，防止无限对穿
    if (~~creep.memory._go?.path?.[0] ===/* 取相反方向 */((direction + 3) % 8 + 1) as DirectionConstant) {
        return ERR_BUSY;
    }

    // 获取对穿规则并进行判断
    const allowCross = crossRules[creep.memory.role] || crossRules.default;
    if (!allowCross(creep, requireCreep)) {
        creep.say('👊');
        return ERR_BUSY;
    }

    // 同意对穿
    creep.say('👌');
    const moveResult = creep.move(direction);
    if (moveResult === OK && creep.memory._go?.path?.length > 0) {
        // 如果移动的方向不是路径中的方向的话，就重新寻路
        if (Number(creep.memory._go.path[0]) as DirectionConstant !== direction) {
            delete creep.memory._go.path;
        }
    }
    return moveResult;
};

/**
 * feat: 请求推开前面的挡路爬
 * 如果爬 A 正在刷墙，有一个运输爬 B 想要通过，此时对穿不行。一般情况下，运输爬要
 * 重新寻路，但代价太高。在可能的情况下，A 只需向墙靠近，就可能给 B 让出位置。本
 * 方法增添了这个功能。
 * 
 * todo: 调查增加 moveCloser 选项后此方法的必要性
 * 
 * @param creep 被请求的爬
 * @param direction 请求移动的方向
 * @param requireCreep 请求 push 的爬
 */
const requirePush = function (creep: AnyCreep, direction: DirectionConstant, requireCreep: AnyCreep) {
    // todo: impl
    // creep 下没有 memory 说明 creep 已经凉了，直接移动即可
    if (!creep.memory) return OK;

    // 检查原来爬的目标，判断是否能够向目标移动
    const requireeTarget = deserializePos(creep.memory._go.targetPos) || getTarget(creep);
    const requireePath = PathFinder.search(creep.pos, requireeTarget, { maxOps: 100 });  // 性能消耗不大
    // 检查路径，如果有，则向目标移动
    if (requireePath.path.length > 0) {
        requireCreep.move(direction);
        const d = creep.pos.getDirectionTo(requireePath.path[0]);
        creep.move(d);
        return OK;
    }
    else {
        return ERR_BUSY;
    }
};

/**
 * 远程寻路
 *
 * @param target 目标位置
 * @param range 搜索范围 默认为 1
 * @returns PathFinder.search 的返回值
 */
const findPath = function (creep: Creep | PowerCreep, target: RoomPosition, moveOpt: MoveOpt = {}): string | undefined {
    // 先查询下缓存里有没有值
    const routeKey = `${creep.pos.serialize()} ${target.serialize()}`;

    if (!moveOpt.disableRouteCache) {
        const route = routeCache[routeKey];
        // 如果有值则直接返回
        if (route) {
            return route;
        }
    }

    const range = moveOpt.range ?? 1;
    const roomCallback = roomName => {
        // 强调了不许走就不走
        if (settings.bypassRooms.includes(roomName)) return false;

        const room = Game.rooms[roomName];
        // 房间没有视野
        if (!room) return;

        // 尝试从缓存中读取，没有缓存就进行查找
        let costs = (roomName in costCache) ? costCache[roomName].clone() : undefined;
        if (!costs) {
            costs = new PathFinder.CostMatrix();
            const terrain = new Room.Terrain(roomName);

            // 设置基础地形 cost
            for (let x = 0; x < 50; x++) {
                for (let y = 0; y < 50; y++) {
                    const tile = terrain.get(x, y);
                    const weight =
                        tile === TERRAIN_MASK_WALL
                            ? 255
                            : tile === TERRAIN_MASK_SWAMP ? 4 : 2;

                    costs.set(x, y, weight);
                }
            }

            const addCost = (item: Structure | ConstructionSite) => {
                // 更倾向走道路
                if (item.structureType === STRUCTURE_ROAD) {
                    // 造好的路可以走，路的工地保持原有 cost
                    if (item instanceof Structure) costs.set(item.pos.x, item.pos.y, 1);
                }
                // 不能穿过无法行走的建筑
                else if (
                    item.structureType !== STRUCTURE_CONTAINER &&
                    (item.structureType !== STRUCTURE_RAMPART || !item['my'])
                ) {
                    costs.set(item.pos.x, item.pos.y, 255);
                }
            };

            // 给建筑和工地添加 cost
            room.find(FIND_STRUCTURES).forEach(addCost);
            room.find(FIND_CONSTRUCTION_SITES).forEach(addCost);

            costCache[roomName] = costs.clone();
        }

        // 躲避房间中的 creep
        room.find(FIND_CREEPS).forEach(otherCreep => {
            // 以下情况会躲避
            if (
                // 如果禁用对穿了
                moveOpt.disableCross ||
                // 或者对方不属于自己
                !otherCreep.my || otherCreep.memory.disableCross ||
                // 或者对穿规则不允许
                !(crossRules[otherCreep.memory.role] || crossRules.default)(otherCreep, creep)
            ) {
                costs.set(otherCreep.pos.x, otherCreep.pos.y, 255);
            }
        });
        // 躲避房间中的非己方 powercreep
        room.find(FIND_POWER_CREEPS).forEach(pc => {
            if (!pc.my) costs.set(pc.pos.x, pc.pos.y, 255);
        });

        return costs;
    };
    const result = PathFinder.search(creep.pos, { pos: target, range }, {
        maxOps: moveOpt.maxOps || 4000,
        roomCallback
    });

    // 如果移动地越近越好，result 也发现了可用路径、 range > 1，就继续寻路
    if (moveOpt.moveCloser && !result.incomplete && moveOpt.range > 1) {
        const nextPath = PathFinder.search(result.path.at(-1), { pos: target, range: 1 }, {
            maxRooms: 1,    // 仅限本房间搜索
            maxOps: 100,    // 限制搜索成本，防止搜索不到消耗大量 CPU
            // heuristicWeight: 8   // todo: investigate this
            roomCallback
        });
        // 不论是否完整，都拼接上
        result.path = result.path.concat(nextPath.path);
    }

    // 可视化寻路
    visualizePath(creep, result);

    // 没找到就返回空
    if (result.path.length <= 0) return undefined;
    // 找到了就进行压缩
    const route = serializeFarPath(creep, result.path);

    // 保存到全局缓存
    if (!result.incomplete) routeCache[routeKey] = route;

    // 根据玩家指定的重用距离返回缓存
    return moveOpt.reusePath ? route.slice(0, moveOpt.reusePath) : route;
};

/** @author UndefinedCpp */
const visualizePath = function (creep: Creep | PowerCreep, path: PathFinderPath) {
    const vs = creep.room.visual;
    // 把爬爬圈起来
    vs.circle(creep.pos.x, creep.pos.y, {
        radius: 0.45,
        stroke: 'orange',
        strokeWidth: 0.15,
        opacity: 0.3,
        fill: 'transparent'
    });
    // 绘制路线
    const pathColor = path.incomplete ? 'red' : 'orange';
    vs.poly([creep.pos].concat(path.path), {
        stroke: pathColor,
        strokeWidth: 0.15,
        opacity: 0.3,
        lineStyle: 'dashed'
    });
}

/**
 * 压缩 PathFinder 返回的路径数组
 *
 * @param positions 房间位置对象数组，必须连续
 * @returns 压缩好的路径
 */
const serializeFarPath = function (creep: Creep | PowerCreep, positions: RoomPosition[]): string {
    if (positions.length === 0) return '';
    // 确保路径的第一个位置是自己的当前位置
    if (!positions[0].isEqualTo(creep.pos)) positions.splice(0, 0, creep.pos);

    return positions.map((pos, index) => {
        // 最后一个位置就不用再移动
        if (index >= positions.length - 1) return null;
        // 由于房间边缘地块会有重叠，所以这里筛除掉重叠的步骤
        if (pos.roomName !== positions[index + 1].roomName) return null;
        // 获取到下个位置的方向
        return pos.getDirectionTo(positions[index + 1]);
    }).join('');
};

/**
 * 显示所有 creep 的移动轨迹
 */
export const visualAllCreepPath = function () {
    Object.values(Game.creeps).forEach(creep => {
        if (!creep.memory._go || !creep.memory._go.path) return;

        const directions: (string | RoomPosition)[] = creep.memory._go.path.split('');
        directions.unshift(creep.pos);
        directions.reduce((pre: RoomPosition, next: string) => {
            const nextPos = directionToPos(pre, Number(next) as DirectionConstant);
            new RoomVisual(pre.roomName).line(pre, nextPos, { color: '#a9b7c6' });
            return nextPos;
        });
    });
};

export const directionToPos = function (origin: RoomPosition, direction: DirectionConstant): RoomPosition | undefined {
    let targetX = origin.x;
    let targetY = origin.y;

    // 纵轴移动，方向朝下就 y ++，否则就 y --
    if (direction !== LEFT && direction !== RIGHT) {
        if (direction > LEFT || direction < RIGHT) targetY--;
        else targetY++;
    }
    // 横轴移动，方向朝右就 x ++，否则就 x --
    if (direction !== TOP && direction !== BOTTOM) {
        if (direction < BOTTOM) targetX++;
        else targetX--;
    }

    // 如果要移动到另一个房间的话就返回空，否则返回目标 pos
    if (targetX < 0 || targetY > 49 || targetX > 49 || targetY < 0) return undefined;
    else return new RoomPosition(targetX, targetY, origin.roomName);
};

export const getCostMatrixOfRoom = (roomName: string) => costCache[roomName] || new PathFinder.CostMatrix();

/**
 * 显示当前所有的路径缓存
 */
export const showRouteChche = function (): string {
    const routeNames = Object.keys(routeCache);
    if (routeNames.length <= 0) return $render('没有路径缓存', ConsoleColor.blue);

    const logs = routeNames.map(routeKey => {
        return `[${routeKey.split(' ').join(' > ')}] ${routeCache[routeKey]}`;
    });

    if (logs.length > 0) {
        logs.unshift($render(`缓存了 ${logs.length} 条路径`, ConsoleColor.blue));
    }
    else return $render('没有路径缓存', ConsoleColor.blue);

    return logs.join('\n');
};